package cn.iocoder.yudao.framework.file.core.client.st;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import io.minio.*;
import io.minio.http.Method;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static cn.iocoder.yudao.framework.file.core.client.st.S3FileClientConfig.ENDPOINT_ALIYUN;
import static cn.iocoder.yudao.framework.file.core.client.st.S3FileClientConfig.ENDPOINT_TENCENT;

/**
 * 基于 S3 协议的文件客户端，实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
 * <p>
 * S3 协议的客户端，采用亚马逊提供的 software.amazon.awssdk.s3 库
 *
 * @author 芋道源码
 */
public class S3FileClient extends AbstractFileClient<cn.iocoder.yudao.framework.file.core.client.st.S3FileClientConfig> {

    private MinioClient client;
    private OSSClient ossClient; // 阿里云 OSS 客户端

    private final ThreadPoolTaskExecutor executor; // 线程池




    public S3FileClient(Long id, cn.iocoder.yudao.framework.file.core.client.st.S3FileClientConfig config) {
        super(id, config);
        // 初始化线程池
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(availableProcessors * 2 / 3); // 设置核心线程数为可用处理器的三分之二
        executor.setMaxPoolSize(availableProcessors * 2); // 最大线程数为可用处理器数的两倍
        executor.setQueueCapacity(50); // 队列容量
        executor.initialize();
    }

    @Override
    protected void doInit() {
        // 补全 domain
        if (StrUtil.isEmpty(config.getDomain())) {
            config.setDomain(buildDomain());
        }
        // 初始化客户端
        client = MinioClient.builder()
                .endpoint(buildEndpointURL()) // Endpoint URL
                .region(buildRegion()) // Region
                .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
                .build();

        ossClient = (OSSClient) new OSSClientBuilder().build(buildEndpointURL(), config.getAccessKey(), config.getAccessSecret());
    }

    /**
     * 基于 endpoint 构建调用云服务的 URL 地址
     *
     * @return URI 地址
     */
    private String buildEndpointURL() {
        // 如果已经是 http 或者 https，则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return config.getEndpoint();
        }
        return StrUtil.format("https://{}", config.getEndpoint());
    }

    /**
     * 基于 bucket + endpoint 构建访问的 Domain 地址
     *
     * @return Domain 地址
     */
    private String buildDomain() {
        // 如果已经是 http 或者 https，则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
        }
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊，必须有自定义域名
        return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
    }

    /**
     * 基于 bucket 构建 region 地区
     *
     * @return region 地区
     */
    private String buildRegion() {
        // 阿里云必须有 region，否则会报错
        if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
            return StrUtil.subBefore(config.getEndpoint(), '.', false)
                    .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀
                    .replaceAll("https://", "");
        }
        // 腾讯云必须有 region，否则会报错
        if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
            return StrUtil.subAfter(config.getEndpoint(), "cos.", false)
                    .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
        }
        return null;
    }

    @Override
    public String upload(byte[] content, String path, String type) throws Exception {
        // 文件大小阈值（比如 16MB），超过该大小则进行分片上传
        int threshold = 16 * 1024 * 1024;
        if (content.length > threshold && config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
            return multipartUpload(content, path, type); // 分片上传
        } else {
            return singleUpload(content, path, type); // 普通上传
        }
    }

    //普通流式上传
    private String singleUpload(byte[] content, String path, String type) throws Exception {
        // 单文件上传
        client.putObject(PutObjectArgs.builder()
                .bucket(config.getBucket()) // bucket 必须传递
                .contentType(type)
                .object(path) // 相对路径作为 key
                .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
                .build());
        // 拼接返回路径
        return config.getDomain() + "/" + path;
    }

    // 分片上传
    public String multipartUpload(byte[] content, String path, String type) throws Exception {
        String uploadId = null;
        try {
            // 创建分片上传请求
            InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest(config.getBucket(), path);
            InitiateMultipartUploadResult initiateResponse = ossClient.initiateMultipartUpload(initiateRequest);
            uploadId = initiateResponse.getUploadId();

            // 计算分片数和分片大小
            int[] partInfo = calculatePartSize(content.length);
            int partCount = partInfo[0];
            int partSize = partInfo[1];

            List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>());
            List<Future<PartETag>> futures = new ArrayList<>();

            // 开始分片上传
            for (int i = 0; i < partCount; i++) {
                int start = i * partSize;
                int size = Math.min(partSize, content.length - start);

                int finalI = i;
                String finalUploadId = uploadId;
                Future<PartETag> future = executor.submit(() -> {
                    UploadPartRequest uploadPartRequest = new UploadPartRequest();
                    uploadPartRequest.setBucketName(config.getBucket());
                    uploadPartRequest.setKey(path);
                    uploadPartRequest.setUploadId(finalUploadId);
                    uploadPartRequest.setPartNumber(finalI + 1);
                    uploadPartRequest.setInputStream(new ByteArrayInputStream(content, start, size));
                    uploadPartRequest.setPartSize(size);
                    return ossClient.uploadPart(uploadPartRequest).getPartETag();
                });
                futures.add(future);
            }

            // 获取所有分片的 ETag
            for (Future<PartETag> future : futures) {
                partETags.add(future.get());
            }

            // 完成分片上传
            CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(config.getBucket(), path, uploadId, partETags);
            ossClient.completeMultipartUpload(completeRequest);

            return config.getDomain() + "/" + path;
        } finally {
            // 无论上传是否成功都关闭线程池
            executor.shutdown();
        }
    }

    // 计算分片数和分片大小
    private int[] calculatePartSize(long fileSize) {
        int maxParts = 8; // 最大分片数
        int minPartSize = 16 * 1024 * 1024; // 最小分片大小 16MB

        // 计算分片数
        int partCount = (int) Math.ceil((double) fileSize / minPartSize);
        partCount = Math.min(partCount, maxParts); // 不超过最大分片数

        // 计算每片大小
        int partSize = (int) Math.ceil((double) fileSize / partCount);

        // 确保每片大小不超过最大分片大小
        if (partSize > 300 * 1024 * 1024 / maxParts) {
            partSize = 300 * 1024 * 1024 / maxParts;
        }

        return new int[]{partCount, partSize};
    }




    @Override
    public void delete(String path) throws Exception {
        client.removeObject(RemoveObjectArgs.builder()
                .bucket(config.getBucket()) // bucket 必须传递
                .object(path) // 相对路径作为 key
                .build());
    }

    @Override
    public byte[] getContent(String path) throws Exception {
        GetObjectResponse response = client.getObject(GetObjectArgs.builder()
                .bucket(config.getBucket()) // bucket 必须传递
                .object(path) // 相对路径作为 key
                .build());
        return IoUtil.readBytes(response);
    }

    @Override
    public cn.iocoder.yudao.framework.file.core.client.st.FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
        String uploadUrl = client.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .method(Method.PUT)
                .bucket(config.getBucket())
                .object(path)
                .expiry(10, TimeUnit.MINUTES) // 过期时间（秒数）取值范围：1 秒 ~ 7 天
                .build()
        );
        return new cn.iocoder.yudao.framework.file.core.client.st.FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);
    }

}
